package com.heima.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.heima.mapper.ApArticleConfigMapper;
import com.heima.mapper.ApArticleContentMapper;
import com.heima.mapper.ApArticleMapper;
import com.heima.minio.MinIoTemplate;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.article.dtos.ArticleHomeDto;
import com.heima.model.article.dtos.SearchWords;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.article.pojos.ApArticleConfig;
import com.heima.model.article.pojos.ApArticleContent;
import com.heima.model.search.dtos.UserSearchDto;
import com.heima.model.search.pojos.SearchArticleVo;
import com.heima.service.ArticleService;
import com.heima.utils.ElasticSearchTemplate;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import net.bytebuddy.asm.Advice;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.AbstractQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryStringQueryBuilder;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.util.*;

/**
 * @author itheima
 * @since 2022-12-31
 */
@Service
public class ArticleServiceImpl implements ArticleService {


    @Autowired
    private ApArticleMapper apArticleMapper;

    @Autowired
    private ApArticleConfigMapper apArticleConfigMapper;

    @Autowired
    private ApArticleContentMapper apArticleContentMapper;

    @Override
    public List<ApArticle> load(ArticleHomeDto dto) {
        // 1.加载首页
        // 前端会在 minBehotTime 传2063的时间
        // publish_time < minBehotTime  and 上架状态

        return apArticleMapper.loadArticle(dto, 1);
    }

    @Override
    public List<ApArticle> loadmore(ArticleHomeDto dto) {

        // 3. 上拉加载
        // 前端会在 minBehotTime 传当前列表中，最底下那篇文章的发布时间
        // publish_time < minBehotTime  and 上架状态

        return apArticleMapper.loadArticle(dto, 1);
    }

    @Override
    public List<ApArticle> loadnew(ArticleHomeDto dto) {
        // 2. 下拉刷新
        // 前端会在 MaxBehotTime 传当前列表中最顶上那片文章的发布时间
        // publish_time > maxBehotTime and 上架状态

        return apArticleMapper.loadArticle(dto, 2);
    }

    @Autowired
    private ElasticSearchTemplate elasticSearchTemplate;

    private void refreshToEs(ApArticle apArticle) throws IOException {

        // 1. 接收入参（刚刚新增/修改的文章信息）ApArticle

        // 2. 调用Es的index方法，将apArticle写进去
        // index：
        // id: 数据的主键
        // source: 数据的内容（JSON）
        elasticSearchTemplate.index("app_info_article2", Long.toString(apArticle.getId()), JSON.toJSONString(apArticle));

    }


    @Override
    @Transactional(rollbackFor = RuntimeException.class)
    public ApArticle saveArticle(ArticleDto dto) throws IOException, TemplateException {
        // 1. 入参判断
        if (dto == null) {
            throw new RuntimeException("入参不能为空");
        }

        // 2.获取dto中的id，根据id判断是修改还是新增
        Long id = dto.getId();

        // 将dto数据转换成我们需要的结构
        ApArticle apArticle = new ApArticle();
        BeanUtils.copyProperties(dto, apArticle);

        apArticle.setFlag((byte) 0);
        apArticle.setLikes(0);
        apArticle.setCollection(0);
        apArticle.setComment(0);
        apArticle.setViews(0);

        apArticle.setCreatedTime(new Date());
        apArticle.setSyncStatus(true);
        apArticle.setOrigin(true);

        // 3. 新增（事务）
        if (id == null) {
            // 3.1 ap_article
            int insertResult = apArticleMapper.insert(apArticle);
            if (insertResult != 1) {
                throw new RuntimeException("插入文章失败");
            }

            // 3.2 ap_article_config
            // 配置数据组装
            ApArticleConfig config = new ApArticleConfig();
            config.setIsDown(false);
            config.setIsDelete(false);
            config.setArticleId(apArticle.getId());
            config.setIsComment(true);
            config.setIsForward(true);

            int insertConfigResult = apArticleConfigMapper.insert(config);
            if (insertConfigResult != 1) {
                throw new RuntimeException("插入文章配置失败");
            }

            // 3.3 ap_article_content
            // 内容数据组装
            ApArticleContent content = new ApArticleContent();
            content.setContent(dto.getContent());
            content.setArticleId(apArticle.getId());

            int insertContentResult = apArticleContentMapper.insert(content);
            if (insertContentResult != 1) {
                throw new RuntimeException("插入文章内容失败");
            }

            // 将新增后的新闻同步给ES
            //refreshToEs(apArticle);

            // 生成html，并且回写回article表
            String html = generateHtml(dto.getContent());
            apArticle.setStaticUrl(html);
            int updateResult = apArticleMapper.updateById(apArticle);
            if (updateResult != 1) {
                throw new RuntimeException("更新文章html失败");
            }
        }

        // 4. 修改（事务）
        else {
            // 4.0 查询文章是否存在
            ApArticle apArticle1 = apArticleMapper.selectById(id);
            if (apArticle1 == null) {
                throw new RuntimeException("文章" + id + "不存在");
            }

            // 4.1 ap_article
            // 将dto中的数据，覆盖到article上就可以了
            BeanUtils.copyProperties(dto, apArticle1);
            int updateResult = apArticleMapper.updateById(apArticle1);
            if (updateResult != 1) {
                throw new RuntimeException("修改文章失败");
            }

            // 4.3 ap_article_content
            QueryWrapper<ApArticleContent> wrapper = new QueryWrapper<>();
            wrapper.eq("article_id", id);

            ApArticleContent apArticleContent = apArticleContentMapper.selectOne(wrapper);
            if (apArticleContent == null) {
                throw new RuntimeException("文章内容不存在");
            }

            apArticleContent.setContent(dto.getContent());
            int updateContentResult = apArticleContentMapper.updateById(apArticleContent);
            if (updateContentResult != 1) {
                throw new RuntimeException("修改文章内容失败");
            }

            apArticle.setId(id);

            // 将新增后的新闻同步给ES
            //refreshToEs(apArticle);

            // 生成html，并且回写回article表
            String html = generateHtml(dto.getContent());
            apArticle1.setStaticUrl(html);
            int updateHtmlResult = apArticleMapper.updateById(apArticle1);
            if (updateHtmlResult != 1) {
                throw new RuntimeException("更新文章html失败");
            }
        }

        return apArticle;
    }


    @Autowired
    private Configuration configuration;

    @Autowired
    private MinIoTemplate minIoTemplate;

    public String generateHtml(String content) throws IOException, TemplateException {
        Template template = null;

        // 调用freemarker生成html页面
        StringWriter out = new StringWriter();
        try {
            // 1. 选择生成页面的模板文件.ftl
            template = configuration.getTemplate("article.ftl");
            // 2. 为模板文件配置数据
            Map<String, Object> contentDataModel = new HashMap<>();
            contentDataModel.put("content", JSON.parseArray(content, Map.class));
            // 3. 合成
            template.process(contentDataModel, out);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 将合并后的html页面的字符流，上传到minio
        InputStream in = new ByteArrayInputStream(out.toString().getBytes());
        String html = minIoTemplate.uploadFile("", Long.toString(System.currentTimeMillis()), in, "html");

        return html;
    }

    @Autowired
    private MongoTemplate mongoTemplate;

    @Override
    public List<SearchArticleVo> searchArticle(UserSearchDto dto) throws IOException {

        int pageSize = dto.getPageSize();
        int pageNum = dto.getPageNum();

        // 本次搜索的关键词
        String searchWords = dto.getSearchWords();

        // 01-10 调用MongoDb，把本次搜索的关键字写入到Mongo中
        SearchWords words = new SearchWords();
        words.setKeyword(searchWords);
        words.setUserId("4");
        words.setCreatedTime(new Date());

        mongoTemplate.save(words);

        // 3. 分页

        // 4. 排序（发布时间）

        // 5. 高亮

        // indexName： 索引名
        // query: 条件构造器
        // highlightBuilder: 高亮构造器
        // skip: 跳过的条数
        // size: 每页显示的条数
        // orderBy: 排序字段
        // orderByType: 排序规则

        // 1. 高亮构造器
        // 参数：高亮字段
        // 参数二：高亮字段的前缀标签
        // 参数三：高亮字段的后缀标签
        HighlightBuilder builder = elasticSearchTemplate.getHighlightBuilder("content", "<em>", "</em>");

        // 2. 根据关键字进行搜索（title,content）
        // 参数一：搜索关键字
        // 参数二：链接条件（是或还是并且）
        // 参数三：搜索的字段
        QueryStringQueryBuilder stringQueryBuilder = elasticSearchTemplate.getStringQueryBuilder(dto.getSearchWords(), Operator.OR, "title", "content");

        // 3. 时间（publishTime<时间）
        Map<String, Object> condition = new HashMap<>();
        condition.put("let", dto.getMinBehotTime());
        RangeQueryBuilder rangeQueryBuilder = elasticSearchTemplate.getRangeBuilder("publishTime", condition);

        // 4. 搜索构造器
        Map<String, AbstractQueryBuilder> map = new HashMap<>();
        map.put("must", stringQueryBuilder);
        // 使用filte的意思是，在前面那个搜索得到的结果集基础上，进行过滤
        map.put("filter", rangeQueryBuilder);
        AbstractQueryBuilder query = elasticSearchTemplate.getBoolQueryBuilder(map);

        // 5. 执行搜索
        SearchResponse search = elasticSearchTemplate.search(
                "app_info_article2",
                query,
                builder,
                (pageNum - 1) * pageSize,
                dto.getPageSize(),
                "publishTime",
                SortOrder.DESC
        );

        List<SearchArticleVo> list = new ArrayList<>();

        SearchHits hits = search.getHits();

        SearchHit[] hits1 = hits.getHits();
        if (hits1 != null && hits1.length > 0) {
            for (SearchHit documentFields : hits1) {
                if (documentFields == null) {
                    continue;
                }

                String sourceAsString = documentFields.getSourceAsString();
                SearchArticleVo searchArticleVo = JSON.parseObject(sourceAsString, SearchArticleVo.class);

                list.add(searchArticleVo);
            }
        }


        return list;
    }

}
